/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.openkunlun.javadsl.springboot.server;

import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.util.ClassUtils;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Awaiting Non-Web Spring Boot {@link ApplicationListener}
 */
final class DaprServerListener implements SmartApplicationListener {

    private static final Integer UNDEFINED_ID = -1;
    private static final String[] WEB_APPLICATION_CONTEXT_CLASSES = new String[]{"org.springframework.web.context.WebApplicationContext", "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext"};
    private static final List<Class<? extends ApplicationEvent>> SUPPORTED_APPLICATION_EVENTS = Arrays.asList(ApplicationReadyEvent.class, ContextClosedEvent.class);

    /**
     * Target the application id
     */
    private final AtomicInteger applicationContextId = new AtomicInteger(UNDEFINED_ID);

    @Override
    public int getOrder() {
        return LOWEST_PRECEDENCE;
    }

    @Override
    public boolean supportsEventType(Class<? extends ApplicationEvent> eventType) {
        return SUPPORTED_APPLICATION_EVENTS.contains(eventType);
    }

    @Override
    public boolean supportsSourceType(Class<?> sourceType) {
        return true;
    }

    @Override
    public void onApplicationEvent(ApplicationEvent event) {
        if (event instanceof ApplicationReadyEvent) {
            onApplicationReadyEvent((ApplicationReadyEvent) event);
        }
    }

    private void onApplicationReadyEvent(ApplicationReadyEvent event) {

        final ConfigurableApplicationContext applicationContext = event.getApplicationContext();
        if (!isRootApplicationContext(applicationContext) || isWebApplicationContext(applicationContext)) {
            return;
        }

        if (applicationContextId.compareAndSet(UNDEFINED_ID, applicationContext.hashCode())) {
            final CountDownLatch signal = new CountDownLatch(1);
            Runnable guard = () -> {
                try {
                    signal.await();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            };
            new Thread(guard).start();
            Runtime.getRuntime().addShutdownHook(new Thread(signal::countDown));
        }
    }

    private boolean isRootApplicationContext(ApplicationContext applicationContext) {
        return applicationContext.getParent() == null;
    }

    private boolean isWebApplicationContext(ApplicationContext applicationContext) {
        boolean webApplication = false;
        for (String contextClass : WEB_APPLICATION_CONTEXT_CLASSES) {
            if (isAssignable(contextClass, applicationContext.getClass(), applicationContext.getClassLoader())) {
                webApplication = true;
                break;
            }
        }
        return webApplication;
    }

    private static boolean isAssignable(String target, Class<?> type, ClassLoader classLoader) {
        try {
            return ClassUtils.resolveClassName(target, classLoader).isAssignableFrom(type);
        } catch (Throwable ex) {
            return false;
        }
    }
}
